package cn.tedu.charging.order.service.impl;

import cn.tedu.charging.common.pojo.JsonResult;
import cn.tedu.charging.common.pojo.param.GunStatusUpdateParam;
import cn.tedu.charging.common.pojo.vo.StationInfoVO;
import cn.tedu.charging.common.pojo.vo.UserInfoVO;
import cn.tedu.charging.common.utils.JsonUtils;
import cn.tedu.charging.order.constant.Constant;
import cn.tedu.charging.order.constant.MqttConstant;
import cn.tedu.charging.order.feign.DeviceClient;
import cn.tedu.charging.order.feign.UserClient;
import cn.tedu.charging.order.job.DeviceCheckJob;
import cn.tedu.charging.order.mqtt.MqttProducer;
import cn.tedu.charging.order.pojo.dto.CarInfoDto;
import cn.tedu.charging.order.pojo.dto.ChargingDto;
import cn.tedu.charging.order.pojo.dto.StationInfoDto;
import cn.tedu.charging.order.pojo.param.OrderAddParam;
import cn.tedu.charging.order.pojo.po.OrderMQPO;
import cn.tedu.charging.order.rabbitmq.RabbitMQOrderProducer;
import cn.tedu.charging.order.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.Random;

/**
 * 订单Service
 */
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired  //测试在根据用户id获取车辆信息中，用MVC自带的restTemplate进行负载均衡访问user方法
    private RestTemplate restTemplate;

    @Autowired
    private UserClient userClient;
    //普通的controller中注入接口OrderService，需要建立专门的实现类OrderServiceImpl，才能执行方法
    //而UserClient没写实现类，却能正常调用方法的原因，就是飞书的feign流程图
    //@EnableFeignClients扫描所有的@FeignClient类，并基于jdk的动态代理，生成一个实现类，进行操作
    @Autowired
    private DeviceClient deviceClient;

    /**
     * 创建订单
     * @param orderAddParam
     * @return
     */
    @Override
    public String createOrder(OrderAddParam orderAddParam) throws Exception{
        //todo 订单创建的流程 自顶向下编程
        //1 创建订单编号
        String orderNo = getOrderNo();
        log.debug("生成订单号:{}",orderNo);

        //2 创建设备自检定时任务
        // 9.1 创建设备自检定时任务   传入订单号、枪ID，定时多长则在里面具体设置
        log.debug("创建自检定时任务:{}",orderNo);
        DeviceCheckJob deviceCheckJob = new DeviceCheckJob(orderNo,orderAddParam.getGunId());
        log.debug("创建自检定时任务成功:{}",orderNo,deviceCheckJob);

        //3 通过枪id获取运营商信息和充电桩信息
        log.debug("获取运营商和场站信息-入参:{}",orderAddParam.getGunId());
        StationInfoDto stationInfo = getStationInfo(orderAddParam.getGunId());
        log.debug("获取运营商和场站信息-入参:{},出参:{}",orderAddParam.getGunId(),stationInfo);

        //4 通过用户id获取用户车辆信息
        log.debug("获取用户车辆信息-入参:{}",orderAddParam.getUserId());
        CarInfoDto carInfo = getCarInfo(orderAddParam.getUserId());
        log.debug("获取用户车辆信息-入参:{},出参:{}",orderAddParam.getUserId(),carInfo);

        //5 通过枪id修改枪的状态为已使用
        // 2 表示为已经使用  就传2
        log.debug("设置充电枪状态为已经使用-入参:枪id-{},状态-{}",orderAddParam.getUserId(),Constant.GUN_STATS_BUSY);
        //Boolean success = updateGunStatus(orderAddParam.getGunId(),2);
        Boolean success = updateGunStatus(orderAddParam.getGunId(), Constant.GUN_STATS_BUSY);
        log.debug("设置充电枪状态为已经使用-入参:枪id-{},状态-{},出参:{}",orderAddParam.getUserId(),Constant.GUN_STATS_BUSY,success);
        if(!success) {
            log.warn("修改枪状态失败!,枪id:{},状态:{}",orderAddParam.getGunId(),2);
        }

        //6 给充电桩发送充电指令
        /*ChargingDto chargingDto = new ChargingDto();
        chargingDto.setOrderNo(orderNo);
        chargingDto.setGunId(orderAddParam.getGunId());
        chargingDto.setPileId(orderAddParam.getPileId());
        chargingDto.setMsg("start");创建订单并不关心什么ChargingDto，只需要让发送充电指令startCharging启动就行，优化掉。总感觉是老师才想起来不需要对象传参*/
        //startCharging(chargingDto);
        startCharging(orderNo,orderAddParam.getPileId(), orderAddParam.getGunId());

        //7 8.2git 发送消息 做延迟订单的处理
        sendOrderMessage(orderNo);

        return orderNo;
    }

    /**
     * 生成订单号
     * 10000 + "_" + 随机数 + "_" + 时间戳
     * @return String
     */
    private String getOrderNo(){
        //换一种算法 雪花算法自己了解   分布式id生成器
        String start = "10000";
        long now = System.currentTimeMillis();
        Random random = new Random();
        int r = random.nextInt(100);
        String orderNo = start + "_" +  r  + "_" + now;
        return orderNo;
    }

    /**
     * 通过枪id获取运营商信息和充电站信息
     * @param gunId
     * @return StationInfoDto
     */
    private StationInfoDto getStationInfo(Integer gunId) {
        //StationInfoDto stationInfoDto = new StationInfoDto();
        //stationInfoDto.setStationId(1); //假数据，通过枪id获取场站信息
        //stationInfoDto.setOperatorId(1);
        JsonResult<StationInfoVO> stationInfo = deviceClient.getStationInfo(gunId); //feign调用方法
        if (stationInfo != null) {  //老师写错了，后面才想起来改成!=
            StationInfoVO data = stationInfo.getData();
            if (data != null) {
                StationInfoDto stationInfoDto = new StationInfoDto();
                BeanUtils.copyProperties(data, stationInfoDto);
                return stationInfoDto;
            }
        }
        return null;
    }

    /**
     * 通过用户id获取用户绑定的车辆信息
     * 先假定一个用户只能绑定一个车辆,充电的时候用户绑定的那辆唯一的车
     * 如果后续功能迭代, 充电的时候有用户选择给哪辆车充电,车的id可以通过入参的方式传入
     * @param userId
     * @return
     */
    private CarInfoDto getCarInfo(Integer userId){
        return getCarInfoByFeign(userId);
    }
    /**
     * 通过feign的方式调用用户服务
     * @param userId
     * @return
     */
    private CarInfoDto getCarInfoByFeign(Integer userId) {
        JsonResult<UserInfoVO> userInfo = userClient.getUserInfo(userId);
        if (userInfo != null) { //老师写错了，后面才想起来改成!=
            UserInfoVO data = userInfo.getData();
            if(data != null) {
                CarInfoDto carInfoDto = new CarInfoDto();
                carInfoDto.setCarId(data.getCarId());
                return carInfoDto;
            }
        }
        return null;
    }

    /**
     * 通过RestTemplate的方式调用用户服务
     * @param userId
     * @return
     */
    private CarInfoDto getCarInfoByRestTemplate(Integer userId){
        String server = chooseUrl();    //测试在根据用户id获取车辆信息中，用restTemplate进行负载均衡访问user方法
        ParameterizedTypeReference<JsonResult<UserInfoVO>> responseBody =
                new ParameterizedTypeReference<JsonResult<UserInfoVO>>() {};
        ResponseEntity<JsonResult<UserInfoVO>> entity =
                restTemplate.exchange(server, HttpMethod.GET, null, responseBody, userId);
        JsonResult<UserInfoVO> body = entity.getBody();
        UserInfoVO data = body.getData();
        Integer carId = data.getCarId();

        CarInfoDto carInfoDto = new CarInfoDto();
        //carInfoDto.setCarId(1); //假数据，通过用户id获取车辆信息
        carInfoDto.setCarId(carId);
        return carInfoDto;
    }
    private String chooseUrl(){
        String url = "http://localhost:8083/user/info/{1}";
        String url1 = "http://localhost:8084/user/info/{1}";
        String url2 = "http://localhost:8085/user/info/{1}";

        //负载均衡 简单的随机实现
        String[] servers = new String[3];
        servers[0] = url;
        servers[1] = url1;
        servers[2] = url2;

        Random random = new Random();
        int r = random.nextInt(servers.length);
        String server = servers[r];
        return server;
    }

    /**
     * 修改充电枪的状态
     * @param gunId 枪id
     * @param gunStatsBusy 枪状态
     * @return
     */
    private Boolean updateGunStatus(Integer gunId,Integer gunStatsBusy){
        GunStatusUpdateParam gunStatusUpdateParam = new GunStatusUpdateParam();
        gunStatusUpdateParam.setId(gunId);
        gunStatusUpdateParam.setStatus(gunStatsBusy);
        JsonResult<Boolean> booleanJsonResult = deviceClient.updateGunStatus(gunStatusUpdateParam);
        if (booleanJsonResult != null) {
            return booleanJsonResult.getData();
        }
        return false;
    }

    @Autowired  //6.2 用mqtt的publish发送消息
    private MqttProducer mqttProducer;
    //好处，用的mqttClient的各种方法，都是定义在MqttProducer中，以后如果mqtt工具改版了，在MqttProducer中修改就行，不用改源代码，是解耦
    /**
     * 异步下发充电指令给充电桩
     * @param orderNo
     * @param pileId
     * @param gunId
     */
    //private void startCharging(ChargingDto chargingDto) {
    private void startCharging(String orderNo,Integer pileId,Integer gunId) {
        log.debug("发送指令:订单号-{},桩id-{},枪id-{}",orderNo,pileId,gunId);
        ChargingDto chargingDto =new ChargingDto();
        chargingDto.setOrderNo(orderNo);
        chargingDto.setPileId(pileId);
        chargingDto.setGunId(gunId);
        chargingDto.setMsg(Constant.START_CHARGING);

        //6.2 补全发送指令的方法，mqttClient.publish的参数是字符串转byte，所以这里把对象转成字符串格式
        //toString既不安全，接收方又不好解析，所以不用
        //String string = chargingDto.toString(); //log和.toString都是打印，是调用类的ToString方法，可以alt+insert写出toString查看，而且toString可以重写只要个别参数，不太安全
        //System.out.println("string: "+string);  //ChargingDto(gunId=2, pileId=2, msg=start, orderNo=...)    充电桩还不好解析
        //mqttProducer.send("/topic/start",string);

        //这里用小刀API创建订单啊
        String json = JsonUtils.toJson(chargingDto);    //json也是一种传输的数据格式。springboot自带的1种默认转换工具Jackson
        //common里写了个JsonUtils的json转换方法
        System.out.println(json);   //{"gunId":2,"pileId":2,"msg":"start","orderNo":"..."}
        // 也可以加断点，debug下查看执行这句时的结果，在Theads&Variables里看到json语句
        //mqttProducer.send("/topic/start",json); //后续这些硬代码都得另外统一放到常量中，便于修改
        //拼接topic 前缀 加上 桩的id
        String topic = MqttConstant.TOPIC_START_CHARGING_PREFIX + chargingDto.getPileId();
        mqttProducer.send(topic,json); // 但桩很多，应该包含有桩id，让桩能清晰的接收指令
        //向主题"/topic/start"发送信息，回头让充电桩订阅这个主题
        //MqttConsumer里订阅的是"/topic/start/result"，获取充电桩的反馈信息
    }


    @Autowired
    private RabbitMQOrderProducer rabbitMQOrderProducer;
    /**
     * 发送消息 git8.2
     * @param orderNo
     */
    private void  sendOrderMessage(String orderNo){ //把订单号穿进来
        //queue队列中应该发送订单id？？不要其他信息了？    因为OrderMQPO要持久化，所以放在po包里？？
        OrderMQPO orderMQPO = new OrderMQPO();
        orderMQPO.setOrderNo(orderNo);
        rabbitMQOrderProducer.sendOrder(orderMQPO);
    }

}
